1-4. 정수·타입변환 계열

관통 질문: 이 숫자가 다른 타입으로 옮겨가는 순간, 조용히 다른 값으로 바뀌지 않는가? C의 정수 연산은 오버플로우가 예외가 아니라 랩어라운드다. signed 오버플로우는 표준상 정의되지 않은 동작이라 컴파일러가 검사 코드를 지우기도 한다. 이 계열의 공통 원칙은 하나: 연산·형변환 “전”에 검사하고, 검증된 값만 다음 타입으로 넘겨라.


17. 정수 오버플로우 CWE-190

신뢰 경계: 외부에서 정해진 size → 오버플로우 검사 없이 → malloc() 할당 크기 ⚠️ malloc(n * size)를 보면 곱셈부터 의심하라. 검사는 계산 “후”가 아니라 “전”에 해야 한다.

공격 시나리오: size 가 오버플로우로 음수가 되어도 malloc() 의 size_t 파라미터로 넘어가면 거대한 양수로 해석되거나 극히 작은 버퍼가 할당되고, 이후 복사에서 힙 오버플로우가 발생한다.

❌ Bad

void* intAlloc(int size, int reserve)
{
    void *rptr;
    size += reserve;                    /* 오버플로우 발생 가능 */
    rptr = malloc(size * sizeof(int));  /* 음수/wrap 값 */
    if (rptr == NULL) exit(1);
    return rptr;
}

✅ Good

void* intAlloc(int size, int reserve)
{
    unsigned s;                         /* 할당 크기는 unsigned */
    size += reserve;
    s = size * sizeof(int);
    if (s < 0) return NULL;             /* 계산 결과를 먼저 검사 */
    rptr = malloc(s);
    if (rptr == NULL) exit(1);
    return rptr;
}

치환 규칙: 할당 크기 파라미터는 unsigned, 곱셈·덧셈 결과는 쓰기 전에 오버플로우 여부 선(先)검사. 곱셈은 x > MAX / y 형태로 나눗셈 이용해 사전 판별.


18. 의도하지 않은 부호 확장 CWE-194

신뢰 경계: int → 검증 없이 → short → unsigned 로 연쇄 변환 ⚠️ 형 변환 전 “음수일 수 있는가”를 먼저 묻고, 뺄셈은 결과가 0 아래로 내려가지 않음을 확인한 뒤 수행하라.

공격 시나리오: 오버플로우로 음수가 된 값을 unsigned 로 옮기면 40억대의 값이 되어 배열 접근이 범위를 크게 벗어나고, 권한 등급 계산에서는 음수가 큰 양수로 뒤집혀 권한 검사를 우회한다.

❌ Bad

short s; unsigned sz;
s = id;                    /* int -> short, 음수화 가능 */
if (s > 256) return 0;     /* 음수는 이 검사를 통과 */
sz = s;                    /* 음수 -> unsigned, 거대한 값 */
return info[sz];           /* 배열 범위 크게 초과 */

✅ Good

int s; unsigned sz;
s = id;
if (s > 256 || s < 0) return 0;      /* 상한 + 음수 검사 */
sz = (unsigned) s;                    /* 검증 후 명시 변환 */
return info[sz];
/* 뺄셈은 결과가 음수가 되지 않는지 먼저 확인 */
if (userLevel > penalty) userLevel -= penalty;
else userLevel = 0;

치환 규칙: 크기가 다른 타입 간 대입 전 명시적 타입 체크, unsigned 변환은 항상 “음수 여부 + 상한” 검사 이후에만 수행.


19. 무부호 정수를 부호 정수로 타입 변환 오류 CWE-196

신뢰 경계: 함수 리턴값(에러=-1) → 부호 확인 없이 → unsigned 크기 인자 ⚠️ 크기·인덱스는 “부호 있는 채로 검증 → 검증 후 unsigned로 변환” 순서를 지키고, 비교 양쪽의 타입을 통일하라.

공격 시나리오: chunkSz() 가 실패로 -1 을 반환하면 unsigned size 가 4294967295 가 되어 memcpy() 가 엄청난 양을 복사, 즉시 메모리가 파괴된다.

❌ Bad

int chunkSz() { if (!initialized) return -1; return chunkSize; }
void* chunkCpy(void *dBuf, void *sBuf) {
    unsigned size;
    size = chunkSz();          /* -1 -> 4294967295 */
    return memcpy(dBuf, sBuf, size); }   /* 즉시 파괴 */

✅ Good

void* chunkCpy(void *dBuf, void *sBuf) {
    int size;                  /* signed 로 받는다 */
    size = chunkSz();
    if (size < 0) return NULL; /* 에러 여부 먼저 확인 */
    return memcpy(dBuf, sBuf, (unsigned)size); }

치환 규칙: 실패를 -1 등 음수로 표현하는 함수의 리턴값은 반드시 signed 로 받아 음수 검사 후에만 unsigned 로 변환.


이 계열 미니 체크리스트

  • malloc(n * size) 형태의 곱셈이 오버플로우 검사 없이 그대로 쓰이는가
  • 큰 타입 → 작은 타입 대입(int→short 등)에서 값이 잘리거나 음수화될 수 있는가
  • 음수를 unsigned 로 변환하기 전에 부호 검사를 했는가
  • 함수 리턴값이 -1(에러)일 수 있는데 unsigned 변수로 즉시 받고 있는가
  • 뺄셈 결과가 음수가 될 가능성을 사전에 배제했는가

관련 노트